package cn.bbstone.pisces2.client.task.impl;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import cn.bbstone.pisces2.client.base.ClientCache;
import cn.bbstone.pisces2.client.task.ITask;
import cn.bbstone.pisces2.client.task.TaskListener;
import cn.bbstone.pisces2.client.task.event.FailEvent;
import cn.bbstone.pisces2.client.task.event.FailEventData;
import cn.bbstone.pisces2.comm.Const;
import cn.bbstone.pisces2.comm.StatusEnum;
import cn.bbstone.pisces2.comm.cache.ClientFliIndexCache;
import cn.bbstone.pisces2.comm.fli.FliSavePoint;
import cn.bbstone.pisces2.comm.recv.RspFileInfo;
import cn.bbstone.pisces2.proto.rsp.RspFile;
import cn.bbstone.pisces2.util.BFileUtil;
import cn.bbstone.pisces2.util.CipherUtil;
import cn.bbstone.pisces2.util.ConfigUtil;
import cn.bbstone.pisces2.util.CtxUtil;
import cn.hutool.core.util.ObjectUtil;

public class FileTask implements ITask {
	private static Logger log = LoggerFactory.getLogger(FileTask.class);

	private List<TaskListener> listener = new ArrayList<>();

	private FileOutputStream fos = null;

	// 4M -> 32M (for 40w+ small files(most file size < 1M) test)
	private int SBUF_SIZE = Const.DEFAULT_CLIENT_RECV_BUFFER_SIZE;

	// accumulate SBUF_SIZE before save file data
	private byte[] sbuf = new byte[SBUF_SIZE];

	// sbuf next write position
	private int spos = 0;

	private String clientFullPath = null;
	private String tempFullPath = null;


	// recv times
	private int recvTimes = 0;
	private RspFileInfo rspInfo;
	private boolean isFileSkip = false;

	public FileTask(RspFileInfo rspFileInfo) {
		init(rspFileInfo);
	}

	/**
	 * when empty file content
	 *
	 * @return
	 */
	private boolean isSameContent(String clientFullPath) {
		boolean bool = false;
		String clientFileChecksum = BFileUtil.checksum(clientFullPath);
		String serverFileChecksum = rspInfo.getChecksum(); // BFileUtil.toStr(rspInfo.getBodyData());

		if (ObjectUtil.isEmpty(rspInfo)) {
			throw new RuntimeException("check file exists client fail. not found rsp file info");
		}
		bool = serverFileChecksum.equals(clientFileChecksum);
		return bool;
	}

	public boolean isFileSkip() {
		return isFileSkip;
	}

	public void addListner(TaskListener listner) {
		listener.add(listner);
	}

	private void init(RspFileInfo rspFileInfo) {
		try {
			// keep response file info in FileTask
			this.rspInfo = rspFileInfo;

			// set recv buffer size
			SBUF_SIZE = ConfigUtil.recvBufferSizeInt(CtxUtil.getConfigModel().getRecvBuffer());
			//
			String rpath = ClientFliIndexCache.getClientIndexNotUpdatePos(rspInfo.getFileNo()).getRpath4Client();
			// escape path special character, have been overridden by FliIndex.getRpath()
//            String rpath = BFileUtil.escapePath(rpath0);
//            log.debug("fileNo: {}, rpath0: {}, escaped rpath: {}", rspInfo.getFileNo(), rpath0, rpath);
			this.clientFullPath = BFileUtil.getClientFullPathLocal(rpath);

			// delete ~/.fli_client/fli.idx file
			if (rspInfo.getFileNo() == 0) {
				// delete file in clientDir
				BFileUtil.deleteFullPath(clientFullPath);
			}
			// create temp file(delete exist first)
			this.tempFullPath = BFileUtil.getClientTempFileFullPath(clientFullPath);
			// delete temporary file ~/.fli_client/fli.idx.part
			if (Files.exists(Paths.get(tempFullPath))) {
				BFileUtil.deleteFullPath(tempFullPath);
			}

			// check if client file exists
			if (rspInfo.getFileNo() > 0 && Files.exists(Paths.get(this.clientFullPath))) {
				// check client file checksum diff from server file checksum
				// not overwrite
				if (!CtxUtil.getConfigModel().isOverwrite()) {
					log.debug("overwrite=false, and file(fileNo: {}) exists, will skip.", rspInfo.getFileNo());
					isFileSkip = true;
					return; // skip file data recv
				}
				if (isSameContent(clientFullPath)) {
					log.debug("overwrite=true, file(fileNo: {}) exist, and with same file content, will skip. ",
							rspInfo.getFileNo());
					isFileSkip = true;
					return; // skip file data recv
				}
				// delete file in clientDir
				BFileUtil.deleteRelClientPath(rpath);
			}

			// create temp file, and open it
			fos = new FileOutputStream(this.tempFullPath, true);
		} catch (FileNotFoundException e) {
			log.error(String.format("File Not Found. clientFullPath: %s, tempFullPath: %s", this.clientFullPath,
					this.tempFullPath), e);
		}
	}

	public StatusEnum appendFileData(RspFile rspData) {
		try {
			byte[] fileData = rspData.getBodyData();
			recvTimes++;
			
			int recvDataLen = fileData.length;
			log.info("appending fileNo:{}, chunkNo/chunks: {}/{}, chunk size: {}", rspData.getFileNo(),
					rspData.getChunkNo(), rspInfo.getChunks(), recvDataLen);
			
			if (recvDataLen <= 0) { // recv empty file, create a file with no data
				log.warn("recv file data is 0.");
				return doAfterAllDataReceived(rspData);
			}
			
			// check body checksum
			String bodyChecksum = CipherUtil.md5(fileData);
			if (!rspData.getChecksum().equals(bodyChecksum)) {
				log.error("received data checksum fail.");
			}
			
			// if cache full/exceed
			if ((spos + recvDataLen) >= SBUF_SIZE) {
				byte[] sbuf2 = new byte[spos + recvDataLen];
				// copy cache data to sbuf2
				System.arraycopy(sbuf, 0, sbuf2, 0, spos);
				// copy new recv data to sbuf2
				System.arraycopy(fileData, 0, sbuf2, spos, recvDataLen);
				doAfterBufferFull(sbuf2);
			} else {
				// cache not full/exceed, append data
				System.arraycopy(fileData, 0, sbuf, spos, recvDataLen);
				// increase sbuf next write pos
				spos += recvDataLen;
			}
			
			// the last chunk(chunkNo: 1~chunks) received
			if (rspData.getChunkNo() == rspInfo.getChunks()) {
//				log.info("chunkNo == chunks, all chunks({}) received.", rspInfo.getChunks());
				return doAfterAllDataReceived(rspData);
			}
			// continue receiving next chunk
			return StatusEnum.CONTINUE;
		} catch (Exception e) {
			log.error("append file data exception.", e);
			closeFos();
			return StatusEnum.ERR_SAVE_DATA;
		}
	}

	private boolean doAfterBufferFull(byte[] sbuf2) {
		return saveToDisk(sbuf2);
	}

	private StatusEnum doAfterAllDataReceived(RspFile rspData) {
		// save the last chunk data(may not full of cache
		if (spos > 0) { // flush receive cache
			byte[] sbuf3 = new byte[spos];
			System.arraycopy(sbuf, 0, sbuf3, 0, spos);
			if (saveToDisk(sbuf3)) {
				// close output stream
				closeFos();
			}
		}

		// print log
		log.info("COMPLETED->fileNo: {}, recv chunks: {}, server chunks: {}", rspInfo.getFileNo(), recvTimes, rspInfo.getChunks());
		// check file integrity
		String serverFileCheckSum = rspInfo.getChecksum();// BByteUtil.toStr(rspInfo.getBodyData()); // bodyData(byte[])
															// is checksum string bytes
		File tempFile = new File(this.tempFullPath);
//        if (rspInfo.getFileNo() > 0) {
		// BUGFIX: file2BytesHex will cause OOM
//            String fileBytesHex = BFileUtil.file2BytesHex(tempFile);
//                log.info("==== tempFile.bytes.length: {}, tempFile.bytes: {} ====", fileBytesHex.length(), fileBytesHex);
//        }
		String clientFileCheckSum = BFileUtil.checksumFile(tempFile);
		log.debug("tempFile.fileSize: {}, checksum: {}, path: {}", tempFile.length(), clientFileCheckSum,
				tempFile.getAbsolutePath());
		// saved temp file content is same as server, rename it
		if (serverFileCheckSum.equals(clientFileCheckSum)) {
			closeFos(); // make sure there are no body used the temp file
			if (BFileUtil.renameCliTempFile(new File(this.tempFullPath), clientFullPath)) {
				log.info("**********>>>temp file rename OK.<<<**********");
			} else {
				log.error("**********>>>temp file rename FAIL.<<<**********");
			}
		} else {
			log.error("recv file checksum error.");
			log.error("fileNo: {}, server checksum: {}", rspInfo.getFileNo(), serverFileCheckSum);
			log.error("fileNo: {}, client checksum: {}", rspData.getFileNo(), clientFileCheckSum);

			FailEvent failEvent = new FailEvent();
			failEvent.setCode("E01");
			failEvent.setFailEventData(new FailEventData(rspInfo.getFileNo(),
					String.format("received file client checksum(%s) not same as server(%s).", clientFileCheckSum,
							serverFileCheckSum)));
			fireFailEvent(failEvent);
			return StatusEnum.ERR_SAVE_DATA;
		}
		long costTime = (System.currentTimeMillis() - rspInfo.getReqTs());
		log.info(">>client request file(fileNo: {}) transfer total cost time: {} ms.<<", rspInfo.getFileNo(), costTime);
		// clean up cache
		fireCompleteEvent();
		//
		return StatusEnum.COMPLETED;
	}

	private void fireFailEvent(FailEvent failEvent) {
		for (TaskListener l : listener) {
			l.onFail(failEvent);
		}
	}

	/**
	 * file data recv complete clean up cache & update save point file
	 *
	 */
	private void fireCompleteEvent() {
		FliSavePoint fliSavePoint = null;
		switch (rspInfo.getMsgType()) {
		case 0x03: // RSP_LIST_INFO (fli.idx)
			// after fli.idx file data recv complete,
			// validate client fli.idx file checksum, if pass, calculate lines count fileNo always 0L
			fliSavePoint = new FliSavePoint();
			fliSavePoint.setFileNo(0L); // fli.idx fileNo = 0
			fliSavePoint.setUpdateTime(System.currentTimeMillis());
			break;
		default: // RS_FILE_DATA
			fliSavePoint = new FliSavePoint();
			fliSavePoint.setFileNo(rspInfo.getFileNo());
			fliSavePoint.setUpdateTime(System.currentTimeMillis());
		}
		for (TaskListener l : listener) {
			l.onCompleted(fliSavePoint);
		}
	}

	/**
	 * skip file TODO only fire clean up cache, donot update savepoint file with
	 * skip file
	 *
	 */
	public void fireSkipEvent() {
		// remove file task of fileNo
		ClientCache.removeTask(rspInfo.getFileNo());
		// update save point (set fileNo to the skiped fileNo
		FliSavePoint fliSavePoint = new FliSavePoint();
		fliSavePoint.setFileNo(rspInfo.getFileNo());
		fliSavePoint.setUpdateTime(System.currentTimeMillis());
		for (TaskListener l : listener) {
			l.onSkip(fliSavePoint);
		}
	}


	private boolean saveToDisk(byte[] data) {
		try {
			fos.write(data);
//			fos.flush();
			log.info("wrote {}B data to disk OK.", data.length);
			
			resetSbuf();
			return true;
		} catch (IOException e) {
			log.error("wrote data to disk error.", e);
			return false;
		}
	}


	private void resetSbuf() {
		spos = 0;
	}

	private void closeFos() {
		try {
			if (fos == null)
				return;
//			fos.flush(); // Flushes this output stream and forces any buffered output bytes to be written
							// out BEFORE close
			fos.close();
			log.debug(">>>>>>>>>>>> successfully close fos for fileNo: {} <<<<<<<<<", rspInfo.getFileNo());
		} catch (IOException e) {
			log.error("close fileOutputStream error.", e);
		}
	}


	public long costTimeMs() {
		return System.currentTimeMillis() - rspInfo.getReqTs();
	}

	public long chunks() {
		return rspInfo == null ? 0L : rspInfo.getChunks();
	}

	public long fileSize() {
		return rspInfo == null ? 0L : rspInfo.getFileSize();
	}

	public String fileFullPath() {
		return this.clientFullPath;
	}
}
